package net.oschina.bilbodai.common.beanutil.bean;

import net.oschina.bilbodai.common.beanutil.core.ASMCodeGenerator;
import net.oschina.bilbodai.common.beanutil.exception.PropertyDumpException;
import net.oschina.bilbodai.common.beanutil.runtime.IRuntimeClassLoader;
import net.oschina.bilbodai.common.beanutil.runtime.RuntimeClassLoader;
import net.oschina.bilbodai.common.beanutil.util.StringUtil;
import net.oschina.bilbodai.common.beanutil.util.TypeUtil;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Opcodes;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.FutureTask;
import java.util.regex.Pattern;

/**
 * @author daimao  Date: 16/4/20 Time: 下午5:35
 * @version $Id$
 */
public class BeanDumpUtil {

    public static final Method PROPERTY_DUMP_METHOD;
    public static final Method PROPERTY_DUMP_METHOD1;
    public static final Method CREATE_ACCESSOR_METHOD;
    public static final String BEAN_DUMP_UTIL_PREFIX = "BeanDumpUtil$";
    public static final String DEFAULT_PROFILE_NAME = "default";
    private static final Pattern VALID_PROFILE_NAME_PATTERN = Pattern.compile("[a-zA-Z0-9_]+");

    static {
        try {
            PROPERTY_DUMP_METHOD = IBeanAccessor.class.getMethod("propertyDump", Object.class, Object.class);
            PROPERTY_DUMP_METHOD1 = IBeanAccessor.class.getMethod("propertyDump", Object.class);
            CREATE_ACCESSOR_METHOD = BeanDumpUtil.class.getMethod("create", Class.class, Class.class, String.class);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException("method not found", e);
        }
    }

    private static IRuntimeClassLoader classLoader;
    private static PrintWriter printWriter;

    static {
        classLoader = AccessController.doPrivileged(new PrivilegedAction<IRuntimeClassLoader>() {

            public IRuntimeClassLoader run() {
                return new RuntimeClassLoader(BeanDumpUtil.class.getClassLoader());
            }

        });
    }

    final static ConcurrentHashMap<String/* path expression */, FutureTask<IBeanAccessor>> cacheProperties =
            new ConcurrentHashMap<String, FutureTask<IBeanAccessor>>();

    public static Object propertyDump(final Object srcBean, final Object targetBean){
        return propertyDump(srcBean, targetBean, classLoader);
    }

    public static Object propertyDump(final Object srcBean, final Object targetBean, final IRuntimeClassLoader classLoader) {
        return propertyDump(srcBean, targetBean, DEFAULT_PROFILE_NAME, classLoader);
    }

    public static Object propertyDump(final Object srcBean, final Object targetBean, final String profile){
        return propertyDump(srcBean, targetBean, profile, classLoader);
    }

    public static Object propertyDump(final Object srcBean, final Object targetBean, final String profile, final IRuntimeClassLoader classLoader) {
        if (srcBean == null || targetBean == null) {
            throw new IllegalArgumentException("src or target bean can't be null");
        }
        return create(srcBean.getClass(), targetBean.getClass(), profile, classLoader).propertyDump(srcBean, targetBean);
    }

    public static <S, T> IBeanAccessor<S, T> create(final Class<? extends S> srcType, final Class<? extends T> targetType){
        return create(srcType, targetType, classLoader);
    }

    public static <S, T> IBeanAccessor<S, T> create(final Class<? extends S> srcType, final Class<? extends T> targetType, final IRuntimeClassLoader classLoader) {
        return create(srcType, targetType, DEFAULT_PROFILE_NAME, classLoader);
    }

    public static <S, T> IBeanAccessor<S, T> create(final Class<? extends S> srcType, final Class<? extends T> targetType, final String profile){
        return create(srcType, targetType, profile, classLoader);
    }

    public static <S, T> IBeanAccessor<S, T> create(final Class<? extends S> srcType, final Class<? extends T> targetType, final String profile, final IRuntimeClassLoader classLoader) {
        if (srcType == null || targetType == null) {
            throw new IllegalArgumentException("srcType or targetType can't be null! srcType=" + srcType + ", targetType=" + targetType);
        }
        checkProfileName(profile);

        String cacheKey = createCacheKey(srcType, targetType, profile);
        FutureTask<IBeanAccessor> task = cacheProperties.get(cacheKey);
        if (task != null) {
            return getCachedProperty(cacheKey, task);
        }
        task = new FutureTask<>(new Callable<IBeanAccessor>() {
            public IBeanAccessor<S, T> call() throws Exception {
                return make(srcType, targetType, profile, classLoader);
            }

        });
        FutureTask<IBeanAccessor> existedTask = cacheProperties.putIfAbsent(cacheKey, task);
        if (existedTask == null) {
            existedTask = task;
            existedTask.run();
        }
        return getCachedProperty(cacheKey, existedTask);
    }

    public static <S, T> IBeanAccessor<S, T> reCreate(final Class<? extends S> srcType, final Class<? extends T> targetType, final String profile, IRuntimeClassLoader classLoader) {
        if (srcType == null || targetType == null) {
            throw new IllegalArgumentException("srcType or targetType can't be null! srcType=" + srcType + ", targetType=" + targetType);
        }
        checkProfileName(profile);
        String cacheKey = createCacheKey(srcType, targetType, profile);
        cacheProperties.remove(cacheKey);
        return create(srcType, targetType, profile, classLoader);
    }

    private static <S, T> IBeanAccessor<S, T> make(Class<? extends S> srcType, Class<? extends T> targetType, final String profile, IRuntimeClassLoader classLoader) throws Exception {
        final String clsName = BEAN_DUMP_UTIL_PREFIX + TypeUtil.getBeanUtilsClassSimpleName(srcType) + "2" + TypeUtil.getBeanUtilsClassSimpleName(targetType) + "1" + profile + "1";
        ASMCodeGenerator asmCodeGenerator = new ASMCodeGenerator(clsName, Opcodes.V1_7, printWriter, Object.class, IBeanAccessor.class) {
            @Override
            public void onVisitorMethods(ClassVisitor cw, Object... args) throws Exception {
                Class<?> srcType = (Class<?>) args[0];
                Class<?> targetType = (Class<?>) args[1];
                new BeanAccessorProcessor()
                        .process(srcType, targetType, profile)
                        .apply(cw);
            }
        };
        Class<?> generateClass = asmCodeGenerator.getLoadedClass(classLoader, srcType, targetType);
        return (IBeanAccessor) generateClass.newInstance();
    }

    private static IBeanAccessor getCachedProperty(final String cacheKey, FutureTask<IBeanAccessor> task) {
        try {
            return task.get();
        } catch (Exception e) {
            cacheProperties.remove(cacheKey);
            throw new IllegalStateException("获取CacheKey为" + cacheKey + "的值出错", e);
        }
    }

    private static <S, T> String createCacheKey(Class<? extends S> srcType, Class<? extends T> targetType, String profile) {
        return srcType.getName() + "->" + targetType.getName() + "[" + profile + "]";
    }

    private static void checkProfileName(String profile) {
        if (StringUtil.isEmpty(profile)) {
            throw new PropertyDumpException("Profile Name can't be empty");
        }
        if (!VALID_PROFILE_NAME_PATTERN.matcher(profile).matches()) {
            throw new PropertyDumpException("Profile Name invalid, only support [a-zA-Z_]+");
        }
    }

    /**
     * 如果要打印class生成的日志，可以设置相应的输出流
     *
     * @param out 输出流
     */
    public static void setTrace(OutputStream out) {
        printWriter = new PrintWriter(new OutputStreamWriter(out));
    }

    public static void close() throws IOException {
        if (printWriter != null) {
            printWriter.close();
        }
    }
}